﻿using System;
using GammaJul.ReSharper.GammaXaml.Psi.CSharp;
using GammaJul.ReSharper.GammaXaml.Psi.Resolve;
using JetBrains.Annotations;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.ExtensionsAPI.Resolve;
using JetBrains.ReSharper.Psi.Resolve;
using JetBrains.ReSharper.Psi.Xaml.Impl.Resolve;
using JetBrains.Util;

namespace GammaJul.ReSharper.GammaXaml.Psi.Xaml {

	/// <summary>
	/// Contains extension methods related to dependency properties.
	/// </summary>
	public static class DpUtil {
		private static readonly Key<DpInitInfo> _dpInitInfoKey = new Key<DpInitInfo>("DpInitInfo");
		private static readonly string[] _registrationMethodNames = Enum.GetNames(typeof(DpRegistrationMethod));

		private const string DpFieldSuffix = "Property";
		private const string DpKeyFieldSuffix = "PropertyKey";
		private const string DpAttachedGetterPrefix = "Get";
		private const string DpAttachedSetterPrefix = "Set";
		
		/// <summary>
		/// Returns the name of a dependency property field based on the property name.
		/// </summary>
		/// <param name="propertyName">The dependency property name.</param>
		/// <returns>The name that a dependency property field for <paramref name="propertyName"/> should have.</returns>
		[NotNull]
		public static string ToDpFieldName([NotNull] this string propertyName) {
			return propertyName + DpFieldSuffix;
		}

		/// <summary>
		/// Returns the name of a dependency property key field based on the property name.
		/// </summary>
		/// <param name="propertyName">The dependency property name.</param>
		/// <returns>The name that a dependency property key field for <paramref name="propertyName"/> should have.</returns>
		[NotNull]
		public static string ToDpKeyFieldName([NotNull] this string propertyName) {
			return propertyName + DpKeyFieldSuffix;
		}

		/// <summary>
		/// Returns the name of a dependency property attached getter based on the property name.
		/// </summary>
		/// <param name="propertyName">The dependency property name.</param>
		/// <returns>The name that a dependency property attached getter for <paramref name="propertyName"/> should have.</returns>
		[NotNull]
		public static string ToDpAttachedGetterName([NotNull] this string propertyName) {
			return DpAttachedGetterPrefix + propertyName;
		}

		/// <summary>
		/// Returns the name of a dependency property attached setter based on the property name.
		/// </summary>
		/// <param name="propertyName">The dependency property name.</param>
		/// <returns>The name that a dependency property attached setter for <paramref name="propertyName"/> should have.</returns>
		[NotNull]
		public static string ToDpAttachedSetterName([NotNull] this string propertyName) {
			return DpAttachedSetterPrefix + propertyName;
		}

		/// <summary>
		/// Returns whether a given string is a dependency property field name.
		/// </summary>
		/// <param name="fieldName">The field name.</param>
		/// <returns><c>true</c> if <paramref name="fieldName"/> ends with Property, <c>false</c> otherwise.</returns>
		public static bool IsDpFieldName([CanBeNull] this string fieldName) {
			return !String.IsNullOrEmpty(fieldName) && fieldName.EndsWith(DpFieldSuffix, StringComparison.Ordinal);
		}

		/// <summary>
		/// Returns whether a given string is a dependency property key field name.
		/// </summary>
		/// <param name="fieldName">The field name.</param>
		/// <returns><c>true</c> if <paramref name="fieldName"/> ends with PropertyKey, <c>false</c> otherwise.</returns>
		public static bool IsDpKeyFieldName([CanBeNull] this string fieldName) {
			return !String.IsNullOrEmpty(fieldName) && fieldName.EndsWith(DpKeyFieldSuffix, StringComparison.Ordinal);
		}

		/// <summary>
		/// Returns whether a given field is a dependency property field.
		/// </summary>
		/// <param name="field">The field to check.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns><c>true</c> if <paramref name="field"/> is a dependency property field, <c>false</c> otherwise.</returns>
		public static bool IsDpField([CanBeNull] this IField field, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			return field != null
				&& xamlTypeCache != null
				&& field.IsStatic
				&& field.ShortName.IsDpFieldName()
			    && Equals(field.Type, xamlTypeCache.DependencyProperty);
		}

		/// <summary>
		/// Returns whether a given field is a dependency property key field.
		/// </summary>
		/// <param name="field">The field to check.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns><c>true</c> if <paramref name="field"/> is a dependency property key field, <c>false</c> otherwise.</returns>
		public static bool IsDpKeyField([CanBeNull] this IField field, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			return field != null
				&& xamlTypeCache != null
				&& field.IsStatic
				&& field.ShortName.IsDpKeyFieldName()
				&& Equals(field.Type, xamlTypeCache.DependencyPropertyKey());
		}

		/// <summary>
		/// Returns whether a given method is a dependency property attached getter.
		/// </summary>
		/// <param name="method">The method to check.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns><c>true</c> if <paramref name="method"/> is a dependency property attached getter, <c>false</c> otherwise.</returns>
		public static bool IsDpAttachedGetter([CanBeNull] this IMethod method, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			return method != null
				&& xamlTypeCache != null
				&& method.IsStatic
				&& method.ShortName.IsDpAttachedGetterName()
				&& method.Parameters.Count == 1
				&& !method.ReturnType.IsVoid();
		}

		/// <summary>
		/// Returns whether a given method is a dependency property attached setter.
		/// </summary>
		/// <param name="method">The method to check.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns><c>true</c> if <paramref name="method"/> is a dependency property attached getter, <c>false</c> otherwise.</returns>
		public static bool IsDpAttachedSetter([CanBeNull] this IMethod method, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			return method != null
				&& xamlTypeCache != null
				&& method.IsStatic
				&& method.ShortName.IsDpAttachedSetterName()
				&& method.Parameters.Count == 2
				&& method.ReturnType.IsVoid();
		}

		/// <summary>
		/// Returns whether a given string is a dependency property attached getter method name.
		/// </summary>
		/// <param name="methodName">The method name.</param>
		/// <returns><c>true</c> if <paramref name="methodName"/> starts with Get, <c>false</c> otherwise.</returns>
		public static bool IsDpAttachedGetterName([CanBeNull] this string methodName) {
			return !String.IsNullOrEmpty(methodName) && methodName.StartsWith(DpAttachedGetterPrefix, StringComparison.Ordinal);
		}

		/// <summary>
		/// Returns whether a given string is a dependency property attached setter method name.
		/// </summary>
		/// <param name="methodName">The method name.</param>
		/// <returns><c>true</c> if <paramref name="methodName"/> starts with Set, <c>false</c> otherwise.</returns>
		public static bool IsDpAttachedSetterName([CanBeNull] this string methodName) {
			return !String.IsNullOrEmpty(methodName) && methodName.StartsWith(DpAttachedSetterPrefix, StringComparison.Ordinal);
		}

		/// <summary>
		/// Gets the expected name of a dependency property based on its field.
		/// </summary>
		/// <param name="field">The field whose expected dependency property name will be returned.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>The name that the dependency property represented by <paramref name="field"/> should have, or <c>null</c> if it couldn't be determined.</returns>
		[CanBeNull]
		public static string GetExpectedDpName([CanBeNull] this IField field, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (field != null && xamlTypeCache != null) {
				string fieldName = field.ShortName;
				if (field.IsDpField(xamlTypeCache))
					return fieldName.Substring(0, fieldName.Length - DpFieldSuffix.Length);
				if (field.IsDpKeyField(xamlTypeCache))
					return fieldName.Substring(0, fieldName.Length - DpKeyFieldSuffix.Length);
			}
			return null;
		}

		/// <summary>
		/// Gets the expected name of a dependency property based on an attached accessor method.
		/// </summary>
		/// <param name="attachedAccessor">The attached accessor method whose expected dependency property name will be returned.</param>
		/// <returns>The name that the dependency property represented by <paramref name="attachedAccessor"/> should have, or <c>null</c> if it couldn't be determined.</returns>
		[CanBeNull]
		public static string GetExpectedDpName([CanBeNull] this IMethod attachedAccessor) {
			if (attachedAccessor != null) {
				string methodName = attachedAccessor.ShortName;
				if (methodName.IsDpAttachedGetterName() || methodName.IsDpAttachedSetterName())
					return methodName.Substring(3);
			}
			return null;
		}
		
		/// <summary>
		/// Gets from cache, or computes from its initializer, information about a dependency property field registration.
		/// </summary>
		/// <param name="fieldDeclaration">The dependency property field declaration.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>Information about <paramref name="fieldDeclaration"/> dependency property registration.</returns>
		[NotNull]
		public static DpInitInfo GetDpOrDpKeyInitInfo([CanBeNull] this IFieldDeclaration fieldDeclaration, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (fieldDeclaration != null && xamlTypeCache != null) {
				DpInitInfo initInfo = fieldDeclaration.UserData.GetData(_dpInitInfoKey);
				if (initInfo == null || !initInfo.IsValid()) {
					initInfo = DigDpOrDpKeyInitInfo(fieldDeclaration, xamlTypeCache) ?? DpInitInfo.UnknownInfo;
					fieldDeclaration.UserData.PutData(_dpInitInfoKey, initInfo);
				}
				return initInfo;
			}
			return DpInitInfo.UnknownInfo;
		}

		/// <summary>
		/// Gets from cache, or computes from its initializer, information about a dependency property field registration.
		/// </summary>
		/// <param name="field">The dependency property field.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>Information about <paramref name="field"/> dependency property registration.</returns>
		[NotNull]
		public static DpInitInfo GetDpOrDpKeyInitInfo([CanBeNull] this IField field, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (field != null && xamlTypeCache != null) {
				foreach (IFieldDeclaration declaration in field.GetDeclarations()) {
					DpInitInfo initInfo = declaration.GetDpOrDpKeyInitInfo(xamlTypeCache);
					if (initInfo.RegistrationMethod != DpRegistrationMethod.Unknown)
						return initInfo;
				}
			}
			return DpInitInfo.UnknownInfo;
		}

		[CanBeNull]
		private static DpInitInfo DigDpOrDpKeyInitInfo([NotNull] IFieldDeclaration declaration, [NotNull] IXamlTypeCache xamlTypeCache) {
			
			// we're searching for a method call as the initializer

			var initializer = declaration.Initial as IExpressionInitializer;
			if (initializer == null)
				return null;

			var invocation = initializer.Value as IInvocationExpression;
			if (invocation == null)
				return null;

			var method = invocation.InvocationExpressionReference.Resolve().DeclaredElement as IMethod;
			if (method == null)
				return null;

			// checks that the initializer method is DependencyProperty.Something()
			if (!Equals(method.GetContainingType(), xamlTypeCache.DependencyProperty.GetTypeElement()))
				return null;

			// checks that the initializer method is a dependency property registration method
			int index = _registrationMethodNames.IndexOf(method.ShortName, StringComparer.Ordinal);
			if (index < 0)
				return null;

			var registrationMethod = (DpRegistrationMethod) index;
			if (registrationMethod == DpRegistrationMethod.Unknown)
				return null;
			
			// gets the corresponding property type argument
			ICSharpArgument argument = invocation.GetArgumentByParameterName("propertyType");
			return new DpInitInfo(registrationMethod, argument != null ? argument.Expression as ITypeofExpression : null);
		}

		/// <summary>
		/// Finds a CLR property wrapping a given dependency property field.
		/// </summary>
		/// <param name="dpField">The dependency property field.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A <see cref="IProperty"/>, or <c>null</c> if none have been found.</returns>
		[CanBeNull]
		public static IProperty GetDpClrProperty([CanBeNull] this IField dpField, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (dpField != null && xamlTypeCache != null) {
				string dpName = dpField.GetExpectedDpName(xamlTypeCache);
				if (dpName != null) {
					return ResolveUtil
						.GetOwnMembersSymbolTable(dpField.GetContainingType(), SymbolTableMode.FULL)
						.Filter(XamlResolveFilters.IsProperty)
						.GetResolveResult(dpName)
						.DeclaredElement as IProperty;
				}
			}
			return null;
		}

		/// <summary>
		/// Finds attached property getter methods on a given type, based on a dependency property name.
		/// </summary>
		/// <param name="typeElement">The type containing potential attached property getters.</param>
		/// <param name="dpName">The name of the dependency property whose getters will be returned.</param>
		/// <returns>An array of methods.</returns>
		[NotNull]
		public static IMethod[] GetDpAttachedGetters([CanBeNull] this ITypeElement typeElement, [CanBeNull] string dpName) {
			if (typeElement != null && !String.IsNullOrEmpty(dpName)) {
				return ResolveUtil
					.GetOwnMembersSymbolTable(typeElement, SymbolTableMode.FULL)
					.Filter(
						XamlResolveFilters.IsMethod,
						XamlResolveFilters.IsStatic,
						new ParameterCountFilter(1),
						NonVoidReturnTypeFilter.Instance)
					.GetSymbolInfos(dpName.ToDpAttachedGetterName())
					.SelectToArray(si => (IMethod) si.GetDeclaredElement());
			}
			return EmptyArray<IMethod>.Instance;
		}

		/// <summary>
		/// Finds attached property getter methods wrapping a given dependency property field.
		/// </summary>
		/// <param name="dpField">The dependency property field.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>An array of methods.</returns>
		[NotNull]
		public static IMethod[] GetDpAttachedGetters([CanBeNull] this IField dpField, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (dpField != null && xamlTypeCache != null)
				return dpField.GetContainingType().GetDpAttachedGetters(dpField.GetExpectedDpName(xamlTypeCache));
			return EmptyArray<IMethod>.Instance;
		}

		/// <summary>
		/// Finds attached property setter methods on a given type, based on a dependency property name.
		/// </summary>
		/// <param name="typeElement">The type containing potential attached property setters.</param>
		/// <param name="dpName">The name of the dependency property whose setters will be returned.</param>
		/// <returns>An array of methods.</returns>
		[NotNull]
		public static IMethod[] GetDpAttachedSetters([CanBeNull] this ITypeElement typeElement, [CanBeNull] string dpName) {
			if (typeElement != null && !String.IsNullOrEmpty(dpName)) {
				return ResolveUtil
					.GetOwnMembersSymbolTable(typeElement, SymbolTableMode.FULL)
					.Filter(
						XamlResolveFilters.IsMethod,
						XamlResolveFilters.IsStatic,
						new ParameterCountFilter(2),
						VoidReturnTypeFilter.Instance)
					.GetSymbolInfos(dpName.ToDpAttachedSetterName())
					.SelectToArray(si => (IMethod) si.GetDeclaredElement());
			}
			return EmptyArray<IMethod>.Instance;
		}

		/// <summary>
		/// Finds attached property setter methods wrapping a given dependency property field.
		/// </summary>
		/// <param name="dpField">The dependency property field.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>An array of methods.</returns>
		[NotNull]
		public static IMethod[] GetDpAttachedSetters([CanBeNull] this IField dpField, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (dpField != null && xamlTypeCache != null)
				return dpField.GetContainingType().GetDpAttachedSetters(dpField.GetExpectedDpName(xamlTypeCache));
			return EmptyArray<IMethod>.Instance;
		}

		/// <summary>
		/// Finds a dependency property field for a given dependency property name.
		/// </summary>
		/// <param name="typeElement">The type containing the dependency property.</param>
		/// <param name="dpName">The dependency property name.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A dependency property field for the dependency property named <paramref name="dpName"/>, or <c>null</c> if none was found.</returns>
		[CanBeNull]
		public static IField GetDpField([CanBeNull] this ITypeElement typeElement, [CanBeNull] string dpName, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (typeElement != null && dpName != null && xamlTypeCache != null) {
				return ResolveUtil
					.GetOwnMembersSymbolTable(typeElement, SymbolTableMode.FULL)
					.Filter(
						XamlResolveFilters.IsField,
						XamlResolveFilters.IsStatic,
						new TypeOwnerFilter(xamlTypeCache.DependencyProperty))
					.GetResolveResult(dpName.ToDpFieldName())
					.DeclaredElement as IField;
			}
			return null;
		}

		/// <summary>
		/// Finds a dependency property field for a given CLR property wrapper.
		/// </summary>
		/// <param name="property">The CLR property.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A dependency property field for the CLR property <paramref name="property"/>, or <c>null</c> if none was found.</returns>
		[CanBeNull]
		public static IField GetDpField([CanBeNull] this IProperty property, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			return property != null && xamlTypeCache != null
				? property.GetContainingType().GetDpField(property.ShortName, xamlTypeCache)
				: null;
		}

		/// <summary>
		/// Finds a dependency property field for a given CLR attached getter or setter method.
		/// </summary>
		/// <param name="attachedAccessor">The CLR attached getter or setter.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A dependency property field for the method <paramref name="attachedAccessor"/>, or <c>null</c> if none was found.</returns>
		[CanBeNull]
		public static IField GetDpField([CanBeNull] this IMethod attachedAccessor, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (attachedAccessor != null && xamlTypeCache != null) {
				string dpName = attachedAccessor.GetExpectedDpName();
				if (dpName != null)
					return attachedAccessor.GetContainingType().GetDpField(dpName, xamlTypeCache);
			}
			return null;
		}

		/// <summary>
		/// Finds a dependency property key field for a given dependency property name.
		/// </summary>
		/// <param name="typeElement">The type containing the dependency property.</param>
		/// <param name="dpName">The dependency property name.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A dependency property key field for the dependency property named <paramref name="dpName"/>, or <c>null</c> if none was found.</returns>
		[CanBeNull]
		public static IField GetDpKeyField([CanBeNull] this ITypeElement typeElement, [CanBeNull] string dpName, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (typeElement != null && dpName != null && xamlTypeCache != null) {
				return ResolveUtil
					.GetOwnMembersSymbolTable(typeElement, SymbolTableMode.FULL)
					.Filter(
						XamlResolveFilters.IsField,
						XamlResolveFilters.IsStatic,
						new TypeOwnerFilter(xamlTypeCache.DependencyPropertyKey()))
					.GetResolveResult(dpName.ToDpKeyFieldName())
					.DeclaredElement as IField;
			}
			return null;
		}

		/// <summary>
		/// Finds a dependency property key field for a given CLR property wrapper.
		/// </summary>
		/// <param name="property">The CLR property.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A dependency property key field for the CLR property <paramref name="property"/>, or <c>null</c> if none was found.</returns>
		[CanBeNull]
		public static IField GetDpKeyField([CanBeNull] this IProperty property, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			return property != null && xamlTypeCache != null
				? property.GetContainingType().GetDpKeyField(property.ShortName, xamlTypeCache)
				: null;
		}

		/// <summary>
		/// Finds a dependency property key field for a given CLR attached getter or setter method.
		/// </summary>
		/// <param name="attachedAccessor">The CLR attached getter or setter.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A dependency property key field for the method <paramref name="attachedAccessor"/>, or <c>null</c> if none was found.</returns>
		[CanBeNull]
		public static IField GetDpKeyField([CanBeNull] this IMethod attachedAccessor, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (attachedAccessor != null && xamlTypeCache != null) {
				string dpName = attachedAccessor.GetExpectedDpName();
				if (dpName != null)
					return attachedAccessor.GetContainingType().GetDpKeyField(dpName, xamlTypeCache);
			}
			return null;
		}

		/// <summary>
		/// Finds a list of all dependency property fields for a given type.
		/// </summary>
		/// <param name="typeElement">The type.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <returns>A list of fields.</returns>
		[NotNull]
		public static IField[] GetDpFields([CanBeNull] this ITypeElement typeElement, [CanBeNull] IXamlTypeCache xamlTypeCache) {
			if (typeElement != null &xamlTypeCache != null) {
				return ResolveUtil
					.GetOwnMembersSymbolTable(typeElement, SymbolTableMode.FULL)
					.Filter(
						XamlResolveFilters.IsField,
						XamlResolveFilters.IsStatic,
						new TypeOwnerFilter(xamlTypeCache.DependencyProperty),
						DpFieldNameFilter.Instance)
					.GetAllSymbolInfos()
					.SelectToArray(si => (IField) si.GetDeclaredElement());
			}
			return EmptyArray<IField>.Instance;
		}

		/// <summary>
		/// Returns whether a given <see cref="DpRegistrationMethod"/> is an attached registration.
		/// </summary>
		/// <param name="registrationMethod">The registration method.</param>
		/// <returns><c>true</c> if <paramref name="registrationMethod"/> if an attached registration method, <c>false</c> otherwise.</returns>
		public static bool IsAttached(this DpRegistrationMethod registrationMethod) {
			return registrationMethod == DpRegistrationMethod.RegisterAttached
				|| registrationMethod == DpRegistrationMethod.RegisterAttachedReadOnly;
		}

		/// <summary>
		/// Returns whether a given <see cref="DpRegistrationMethod"/> is a read-only registration.
		/// </summary>
		/// <param name="registrationMethod">The registration method.</param>
		/// <returns><c>true</c> if <paramref name="registrationMethod"/> if a read-only registration method, <c>false</c> otherwise.</returns>
		public static bool IsReadOnly(this DpRegistrationMethod registrationMethod) {
			return registrationMethod == DpRegistrationMethod.RegisterReadOnly
				|| registrationMethod == DpRegistrationMethod.RegisterAttachedReadOnly;
		}

	}

}